using System;
using System.CodeDom;
using System.Collections;
using System.IO;
using System.Text;
using EnvDTE;
using CodeNamespace=EnvDTE.CodeNamespace;

namespace NUnitGenAddIn.Core
{
    /// <summary>
    /// Unit Test Generator
    /// </summary>
	public class UnitTestGenerator
	{
        /// <summary>
        /// Add-in Helper
        /// </summary>
		protected AddInHelper _addInHelper;

        /// <summary>
        /// Unit Test Generator Helper
        /// </summary>
		protected UnitTestGeneratorHelper _unitTestGeneratorHelper;

        /// <summary>
        /// Code Dom Helper
        /// </summary>
		protected CodeDomHelper _codeDomHelper;

        /// <summary>
        /// Initializes a new instance of the <see cref="UnitTestGenerator"/> class.
        /// </summary>
        /// <param name="addInHelper">The add in helper.</param>
        /// <param name="unitTestGeneratorHelper">The unit test generator helper.</param>
        /// <param name="codeDomHelper">The code DOM helper.</param>
		public UnitTestGenerator(AddInHelper addInHelper, UnitTestGeneratorHelper unitTestGeneratorHelper,
		                         CodeDomHelper codeDomHelper)
		{
			this._addInHelper = addInHelper;
			this._unitTestGeneratorHelper = unitTestGeneratorHelper;
			this._codeDomHelper = codeDomHelper;
		}

        /// <summary>
        /// Generates the N unit test4 selection.
        /// </summary>
        /// <param name="targetSelection">The target selection.</param>
		public void GenerateNUnitTest4Selection(TextSelection targetSelection)
		{
			ArrayList targetCodeElementList = new ArrayList();
			if (targetSelection.IsEmpty)
				targetCodeElementList = _addInHelper.GetCodeElementListFromPoint(targetSelection.ActivePoint);
	
			if (targetCodeElementList.Count == 0)
			{
				_addInHelper.ShowErrorMessageBox("Cursor is not in the proper position or selection is invalid!");
			}
			else
			{
                //CodeNamespace targetNamespaceObj = _addInHelper.GetNameSpaceFromCodeElementList(targetCodeElementList);
				ArrayList targetFunctionList = _addInHelper.GetFunctionListFromCodeElementList(targetCodeElementList);
				ArrayList targetClassList = _addInHelper.GetClassListFromCodeElementList(targetCodeElementList);
				generateUnitTests(targetClassList, targetFunctionList);
			}
		}

        /// <summary>
        /// Generates the <see cref="NUnit"/> test for the specific item.
        /// </summary>
        /// <param name="selectedItems">The selected items.</param>
		public void GenerateNUnitTest4Item(SelectedItems selectedItems)
		{
			if (selectedItems != null && selectedItems.Count > 0)
			{
				foreach (SelectedItem item in selectedItems)
				{
					generateTestFixtureForItem(item.ProjectItem);
				}
			}
		}

        /// <summary>
        /// Generates the <see cref="NUnit"/> test for the project.
        /// </summary>
        /// <param name="selectedItems">The selected items.</param>
		public void GenerateNUnitTest4Project(SelectedItems selectedItems)
		{
			if (selectedItems != null && selectedItems.Count > 0)
			{
				foreach (SelectedItem selectedItem in selectedItems)
				{
					Project project = selectedItem.Project;
					if (project != null)
					{
						//Make copy of list to prevent recursive generation
						ArrayList projectItemList = new ArrayList();
						foreach (ProjectItem projectItem in project.ProjectItems)
						{
							addProjectItemToListRecursively(projectItemList, projectItem);
						}

                        ProgressHelper.Maximum = projectItemList.Count;
                        ProgressHelper.ShowProgress();
                        try
                        {
                            foreach (ProjectItem projectItem in projectItemList)
                            {
                                ProgressHelper.PerformStep();
                                generateTestFixtureForItem(projectItem);
                            }
                        }
                        finally
                        {
                            ProgressHelper.CloseProgress();
                        }
					}
				}
			}
		}

        /// <summary>
        /// Determines whether the specified document is supported.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <returns>
        /// 	<c>true</c> if the specified document is supported; otherwise, <c>false</c>.
        /// </returns>
		public bool IsSupportedDocument(Document document)
		{
			return document.Language.Equals("CSharp");
		}

        /// <summary>
        /// Includes the supported item.
        /// </summary>
        /// <param name="selectedItems">The selected items.</param>
        /// <returns></returns>
		public bool IncludesSupportedItem(SelectedItems selectedItems)
		{
			foreach (SelectedItem item in selectedItems)
			{
				if (item.Project != null && isSupportedProject(item.Project))
				{
				    return true;
				}

				if (item.ProjectItem != null && isSupportedItem(item.ProjectItem))
				{
				    return true;
				}
			}

			return false;
		}

		#region Virtual Protected Methods
        /// <summary>
        /// Gets the type list from code elements.
        /// </summary>
        /// <param name="typeList">The type list.</param>
        /// <param name="codeElements">The code elements.</param>
		protected virtual void GetTypeListFromCodeElements(ArrayList typeList, CodeElements codeElements)
		{
			foreach (CodeElement element in codeElements)
			{
				if (element.Kind == vsCMElement.vsCMElementClass)
				{
					CodeClass type = element as CodeClass;
					if (IsTypeEligibleForUnitTestGeneration(type))
						typeList.Add(type);
				}
				else if (element.Kind == vsCMElement.vsCMElementNamespace)
				{
				    CodeNamespace cn = element as CodeNamespace;
				    if (cn != null) 
                        GetTypeListFromCodeElements(typeList, cn.Members);
				}
			}
		}

        /// <summary>
        /// Generates the test method.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="testClass">The test class.</param>
        /// <param name="testMethodName">Name of the test method.</param>
		protected virtual void GenerateTestMethod(CodeFunction targetFunction, CodeClass testClass, string testMethodName)
		{
			throw new NotImplementedException(
				"Due to different implementation of CodeFunction.AddAttribute in EnvDTE and EnvDTE2, you must implement this function.");
		}

        /// <summary>
        /// Adds the new test fixture file.
        /// </summary>
        /// <param name="testClassName">Name of the test class.</param>
        /// <param name="targetClass">The target class.</param>
		protected virtual void AddNewTestFixtureFile(string testClassName, CodeClass targetClass)
		{
			throw new NotImplementedException(
				"Due to different implementation of Solution.GetProjectItemTemplate in EnvDTE and EnvDTE2, you must implement this function.");
		}

        /// <summary>
        /// Gets the new name of the test method.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="targetFunctionList">The target function list.</param>
        /// <returns></returns>
		protected string GetNewTestMethodName(CodeFunction targetFunction, ArrayList targetFunctionList)
		{
			const string prefix = "Test";
			string testMethodName = prefix;
			if(IsFunctionConstructor(targetFunction))
			{
			    testMethodName += "Constructor";
			}

			testMethodName += targetFunction.Name;
			
			if (targetFunction.IsOverloaded)
			{
				foreach(CodeParameter param in targetFunction.Parameters)
				{
					testMethodName += GetPascalCaseName(param.Name);
				}
			}

			return testMethodName;
		}

        /// <summary>
        /// Determines whether the specified code function is an constructor.
        /// </summary>
        /// <param name="codeFunction">The code function.</param>
        /// <returns>
        /// 	<c>true</c> if the specified code function is an constructor; otherwise, <c>false</c>.
        /// </returns>
		protected virtual bool IsFunctionConstructor(CodeFunction codeFunction)
		{
			throw new NotImplementedException(
				"Due to different implementation of CodeFunction.FunctionKind in EnvDTE and EnvDTE2, you must implement this function.");
		}

        /// <summary>
        /// Generates the test method body statements from target function.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <returns></returns>
		protected virtual CodeStatementCollection GenerateTestMethodBodyStatementsFromTargetFunction(CodeFunction targetFunction)
		{
			throw new NotImplementedException(
				"Due to different implementation of CodeFunction.FunctionKind in EnvDTE and EnvDTE2, you must implement this function.");
		}

        /// <summary>
        /// Processes the parameters.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="statementCollection">The statement collection.</param>
		protected virtual void ProcessParameters(CodeFunction targetFunction, CodeStatementCollection statementCollection)
		{
			throw new NotImplementedException(
				"Due to different implementation of CodeParameter.ParameterKind in EnvDTE and EnvDTE2, you must implement this function.");
		}

        /// <summary>
        /// Prepares the parameters.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="statementCollection">The statement collection.</param>
        /// <returns>Array of <see cref="CodeExpression"/></returns>
		protected virtual CodeExpression[] PrepareParameters(CodeFunction targetFunction,
			                                                 CodeStatementCollection statementCollection)
		{
			throw new NotImplementedException(
				"Due to different implementation of CodeParameter.ParameterKind in EnvDTE and EnvDTE2, you must implement this function.");
		}
		#endregion

		#region Protected Methods
        /// <summary>
        /// Determines whether if the specified type is eligible for unit test generation.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>
        /// 	<c>true</c> if type is eligible for unit test generation; otherwise, <c>false</c>.
        /// </returns>
		protected static bool IsTypeEligibleForUnitTestGeneration(CodeClass type)
		{
			bool eligibleType = true;

			foreach (CodeElement e in type.Attributes)
			{
				if (e.Name == "TestFixture")
				{
				    eligibleType = false;
				}
			}

			return eligibleType;
		}

        /// <summary>
        /// Gets the name of the pascal case.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <returns>Pascal case name</returns>
		protected static string GetPascalCaseName(string name)
		{
			return name[0].ToString().ToUpper() + name.Substring(1);
		}

        /// <summary>
        /// Generates the test method body from target function.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <returns></returns>
		protected string GenerateTestMethodBodyFromTargetFunction(CodeFunction targetFunction)
		{
			CodeStatementCollection statementCollection = GenerateTestMethodBodyStatementsFromTargetFunction(targetFunction);
			StringBuilder code = new StringBuilder();
			foreach (CodeStatement statement in statementCollection)
			{
				code.Append(generateCodeFromCodeStatement(statement));
			}

			return code.ToString();
		}

        /// <summary>
        /// Processes the other method.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="targetTypeName">Name of the target type.</param>
        /// <param name="statementCollection">The statement collection.</param>
        /// <param name="funcParamsArray">The function parameters array.</param>
		protected void ProcessOtherMethod(CodeFunction targetFunction, string targetTypeName,
		                                  CodeStatementCollection statementCollection, CodeExpression[] funcParamsArray)
		{
			CodeExpression methodInvokeStatement =
				new CodeMethodInvokeExpression(getTargetObjectExpression(targetFunction, targetTypeName), targetFunction.Name,
				                               funcParamsArray);

			if (targetFunction.Type.TypeKind != vsCMTypeRef.vsCMTypeRefVoid)
			{
			    processReturingMethod(targetFunction, statementCollection, methodInvokeStatement);
			}
			else
			{
			    statementCollection.Add(new CodeExpressionStatement(methodInvokeStatement));
			}

			ProcessParameters(targetFunction, statementCollection);
		}

        /// <summary>
        /// Processes the constructor.
        /// </summary>
        /// <param name="targetTypeName">Name of the target type.</param>
        /// <param name="statementCollection">The statement collection.</param>
        /// <param name="funcParamsArray">The func params array.</param>
		protected void ProcessConstructor(string targetTypeName, CodeStatementCollection statementCollection,
			                              CodeExpression[] funcParamsArray)
		{
			string instanceName = "test" + targetTypeName;
			statementCollection.Add(
				new CodeVariableDeclarationStatement(new CodeTypeReference(targetTypeName), instanceName,
				new CodeObjectCreateExpression(targetTypeName, funcParamsArray)));

			statementCollection.Add(
				_unitTestGeneratorHelper.CreateIsNotNullMethodInvokeExpression(instanceName,
				"Constructor of type, " + targetTypeName +
				" failed to create instance."));
		}
		#endregion

		#region Private Methods

        /// <summary>
        /// Processes the returning method.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="statementCollection">The statement collection.</param>
        /// <param name="methodInvokeStatement">The method invoke statement.</param>
		private void processReturingMethod(CodeFunction targetFunction, CodeStatementCollection statementCollection,
			CodeExpression methodInvokeStatement)
		{
			string expectedName = "expected" + targetFunction.Type.CodeType.Name;
			string resultName = "result" + targetFunction.Type.CodeType.Name;
			statementCollection.Add(
				new CodeVariableDeclarationStatement(targetFunction.Type.AsFullName, expectedName, new CodePrimitiveExpression(null)));
			statementCollection.Add(
				new CodeVariableDeclarationStatement(targetFunction.Type.AsFullName, resultName, new CodePrimitiveExpression(null)));
			statementCollection.Add(
				new CodeAssignStatement(new CodeVariableReferenceExpression(resultName), methodInvokeStatement));
			statementCollection.Add(
				_unitTestGeneratorHelper.CreateAreEqualMethodInvokeExpression(expectedName, resultName,
				targetFunction.Name +
				" method returned unexpected result."));
		}

        /// <summary>
        /// Gets the target object expression.
        /// </summary>
        /// <param name="targetFunction">The target function.</param>
        /// <param name="targetTypeName">Name of the target type.</param>
        /// <returns></returns>
		private CodeExpression getTargetObjectExpression(CodeFunction targetFunction, string targetTypeName)
		{
			return targetFunction.IsShared
				   ? new CodeTypeReferenceExpression(targetTypeName)
				   : new CodeFieldReferenceExpression(null, _unitTestGeneratorHelper.UnitUnderTestMemberFieldName) as CodeExpression;
		}

        /// <summary>
        /// Generates the new test fixture.
        /// </summary>
        /// <param name="targetNamespace">The target <c>Namespace</c>.</param>
        /// <param name="targetClass">The target class.</param>
        /// <param name="testClassName">Name of the test class.</param>
        /// <returns></returns>
		private ProjectItem generateNewTestFixture(string targetNamespace, CodeClass targetClass, string testClassName)
		{
			AddNewTestFixtureFile(testClassName, targetClass);
			ProjectItem testProjectItem = _addInHelper.GetCurrentProjectItem();

            TextSelection testSelection;
			try
			{
				testSelection = _addInHelper.GetActiveDocument().Selection as TextSelection;
			    if (testSelection != null)
			    {
			        string val = _unitTestGeneratorHelper.GenerateInitialUnitTestClassCode(targetNamespace, targetClass, testClassName);
                    testSelection.Insert(val, (int)vsInsertFlags.vsInsertFlagsInsertAtStart);                    
			    }
                
			    _addInHelper.GetActiveDocument().Save("");
                
                _addInHelper.ExecuteCommand("Edit", "FormatDocument", "");
                _addInHelper.GetActiveDocument().Save("");
			}
			catch (Exception)
			{}

			return testProjectItem;
		}

        /// <summary>
        /// Check if the Test already exists.
        /// </summary>
        /// <param name="testFunctionName">Name of the test function.</param>
        /// <param name="testClass">The test class.</param>
        /// <returns>true if test exist else false</returns>
		private static bool testAlreadyExists(string testFunctionName, CodeClass testClass)
		{
			foreach (CodeElement e in testClass.Members)
			{
				if (e.Kind == vsCMElement.vsCMElementFunction && e.Name == testFunctionName)
				{
				    return true;
				}
			}

			return false;
		}
        
        /// <summary>
        /// Generates the code from code statement.
        /// </summary>
        /// <param name="statement">The statement.</param>
        /// <returns></returns>
		private string generateCodeFromCodeStatement(CodeStatement statement)
		{
			StringBuilder code = new StringBuilder();
			StringWriter writer = new StringWriter(code);
			_codeDomHelper.GenerateCodeFromStatement(statement, writer, null);

			return code.ToString();
		}

        /// <summary>
        /// Adds the project item to list recursively.
        /// </summary>
        /// <param name="projectItemList">The project item list.</param>
        /// <param name="projectItem">The project item.</param>
		private static void addProjectItemToListRecursively(IList projectItemList, ProjectItem projectItem)
		{
			projectItemList.Add(projectItem);
			if (projectItem.ProjectItems != null && projectItem.ProjectItems.Count > 0)
			{
				foreach (ProjectItem item in projectItem.ProjectItems)
				{
					addProjectItemToListRecursively(projectItemList, item);
				}
			}
		}

        /// <summary>
        /// Generates the test fixture for item.
        /// </summary>
        /// <param name="projectItem">The project item.</param>
		private void generateTestFixtureForItem(ProjectItem projectItem)
		{
			if (projectItem != null && isSupportedItem(projectItem))
			{
				ArrayList typeList = new ArrayList();
				GetTypeListFromCodeElements(typeList, projectItem.FileCodeModel.CodeElements);
				generateUnitTests(typeList);
			}
		}

        /// <summary>
        /// Determines whether the specified project item is supported.
        /// </summary>
        /// <param name="projectItem">The project item.</param>
        /// <returns>
        /// 	<c>true</c> if the specified project item is supported; otherwise, <c>false</c>.
        /// </returns>
		private static bool isSupportedItem(ProjectItem projectItem)
		{
			return projectItem.Name != null && projectItem.Name.EndsWith(".cs");
		}

        /// <summary>
        /// Determines whether the specified project is supported.
        /// </summary>
        /// <param name="project">The project.</param>
        /// <returns>
        /// 	<c>true</c> if the specified project is supported; otherwise, <c>false</c>.
        /// </returns>
		private static bool isSupportedProject(Project project)
		{
			foreach (ProjectItem item in project.ProjectItems)
			{
				if (isSupportedItem(item))
					return true;
			}
			return false;
		}

        /// <summary>
        /// Generates the unit tests.
        /// </summary>
        /// <param name="targetTypeList">The target type list.</param>
        /// <param name="targetFunctionList">The target function list.</param>
		private void generateUnitTests(ArrayList targetTypeList, ArrayList targetFunctionList)
		{
			if (targetFunctionList.Count > 0)
			{
				CodeClass targetClass = ((CodeFunction) targetFunctionList[0]).Parent as CodeClass;

			    if (targetClass != null)
			    {
			        string targetNamespace = ((CodeNamespace) targetClass.Parent).FullName;
			        ArrayList filteredTargetFunctionList =
			            _unitTestGeneratorHelper.GetCandidateMethodListFromFunctionList(targetFunctionList);

			        if(filteredTargetFunctionList.Count > 0)
			        {
                        generateUnitTestsFromTargetFunctionList(targetNamespace, targetClass, filteredTargetFunctionList);
			        }
			        else
			        {
			            _addInHelper.ShowInformationMessageBox("No method found for generating Unit Test.");
			        }
			    }
			}
			else if (targetTypeList.Count > 0)
			{
				generateUnitTests(targetTypeList);
			}
		}

        /// <summary>
        /// Generates the unit tests.
        /// </summary>
        /// <param name="targetTypeList">The target type list.</param>
		private void generateUnitTests(ArrayList targetTypeList)
        {
            foreach (CodeClass targetClass in targetTypeList)
            {                    
                string targetNamespace = ((CodeNamespace)targetClass.Parent).FullName;
                ArrayList targetFunctionList = _unitTestGeneratorHelper.GetCandidateMemberMethodListFromClass(targetClass);
                generateUnitTestsFromTargetFunctionList(targetNamespace, targetClass, targetFunctionList);
            }
        }

        /// <summary>
        /// Generates the unit tests from target function list.
        /// </summary>
        /// <param name="targetNamespace">The target <c>Namespace</c>.</param>
        /// <param name="targetType">Type of the target.</param>
        /// <param name="targetFunctionList">The target function list.</param>
		private void generateUnitTestsFromTargetFunctionList(string targetNamespace, CodeClass targetType,
		                                                     ArrayList targetFunctionList)
		{
			if (targetFunctionList == null || targetFunctionList.Count == 0)
			{
			    return;
			}

			string testClassName = (targetType.Name.Length > 0
					               ? targetType.Name
					               : Path.GetFileNameWithoutExtension(targetType.ProjectItem.Name)) + "Test";

			ProjectItem testProjectItem = null;
			Project targetProject = targetType.ProjectItem.ContainingProject;
			try
			{
				testProjectItem = _addInHelper.GetProjectItemForClassFromProject(targetNamespace, testClassName, targetProject);
			}
			catch (Exception)
			{}

			if (testProjectItem == null)
			{
			    testProjectItem = generateNewTestFixture(targetNamespace, targetType, testClassName);
			}

			try
			{
				if (!testProjectItem.get_IsOpen(Constants.vsViewKindCode))
				{
				    testProjectItem.Open(Constants.vsViewKindCode);
				}

				testProjectItem.Document.Activate();
			}
			catch (Exception)
			{}

		    CodeClass testClass = _addInHelper.GetCodeClassFromCodeElements(_addInHelper.GetCodeModel4CurrentProjectItem().CodeElements);

			if (testClass != null && targetFunctionList.Count > 0)
			{
			    foreach (CodeFunction targetFunction in targetFunctionList)
			    {
			        string testMethodName = GetNewTestMethodName(targetFunction, targetFunctionList);
			        if (!testAlreadyExists(testMethodName, testClass))
			        {
			            GenerateTestMethod(targetFunction, testClass, testMethodName);
			        }
			        else
			        {
			            testDocumentationExists(testMethodName, testClass);
			        }
			    }
			}
		}

        /// <summary>
        /// Tests the documentation exists.
        /// </summary>
        /// <param name="testFunctionName">Name of the test function.</param>
        /// <param name="testClass">The test class.</param>
        /// <returns></returns>
        private static void testDocumentationExists(string testFunctionName, CodeClass testClass)
        {
            foreach (CodeElement e in testClass.Members)
            {
                if (e.Kind == vsCMElement.vsCMElementFunction && e.Name.Equals(testFunctionName))
                {
                    CodeFunction function = (CodeFunction) e;
                    if (function.DocComment.Equals("<doc>\r\n</doc>"))
                    {
                        function.DocComment = DocumentationHelper.GetDocumentationHeaderString(testFunctionName);
                    }
                }
            }            
        }

        #endregion
	}
}